#pragma once

#include	<google/protobuf/message.h>
#include	<google/protobuf/descriptor.h>
#include	<redis++.h>
#include	<Singleton.h>
#include	<Logger.h>

#define		GCache	Cache::Instance()

/**
 * Cache manager.
 */
class Cache : public ISingleton<Cache> {
public:
	Cache() {}
	virtual ~Cache() {}

	/**
	 * Initialize cache manager.
	 *
	 * \param	redis_host	Host of redis server.
	 * \param	redis_port	Port to connect to redis.
	 * \return Initialize status.
	 */
	bool Initialize(const std::string & redis_host, int redis_port);

	/**
	 * Get low level of redis client.
	 */
	Redis::Client & Context() { return _redis; }

	/**
	 * Cache data into redis only.
	 *
	 * \param	id		Unique id to get data.
	 * \param	data	Data to cache.
	 * \return Request status.
	 */
	bool Save(uint64_t id, const google::protobuf::Message & data);

	/**
	 * Cache data into redis & push flush task(INSERT INTO database).
	 *
	 * \param	id		Unique id to get data.
	 * \param	data	Data to cache & flush.
	 * \return Request status.
	 */
	bool Flush(uint64_t id, const google::protobuf::Message & data);

	/**
	 * Cache data into redis & push flush task(UPDATE data in database)
	 *
	 * \param	id		Unique id to get data.
	 * \param	data	Data to cache & flush.
	 * \param	primary	Primary key in database & data. Used to generate "WHERE $primary=data[$primary]"
	 * \return Request status.
	 */
	bool Flush(uint64_t id, const google::protobuf::Message & data, const std::string & primary);

	/**
	 * Tick redis message loop.
	 */
	void Breath();

	/**
	 * Get cached google::protobuf::Message. For simple type, using Cache::Context().Command directly.
	 *
	 * \param	id		Unique id.
	 * \param	handler	Callback
	 */
	template<class T>
	bool Get(uint64_t id, const std::function<void(T *)> & handler) {
		const google::protobuf::Descriptor * desc = T::descriptor();
		std::string cache_key = desc->name() + "_" + std::to_string(id);

		return _redis.Command([=](Redis::Reply & reply) {
			if (!reply) {
				LOG_ERR("Cache::Get failed to get : %s. Error : %s", cache_key.c_str(), reply.AsError());
				handler(nullptr);
			} else if (reply.IsNil()) {
				handler(nullptr);
			} else if (!reply.IsString()) {
				LOG_WARN("Cache::Get data in key %s is NOT google::protobuf::Message", cache_key.c_str());
				handler(nullptr);
			} else {
				T data;
				if (data.ParseFromString(reply.AsString())) {
					handler(&data);
				} else {
					LOG_WARN("Cache::Get data in key %s is NOT google::protobuf::Message due to parse", cache_key.c_str());
					handler(nullptr);
				}
			}
		}, "GET %s", cache_key.c_str());
	}

private:
	Redis::Client _redis;
};